Skip to content

Conversation

@devin-ai-integration
Copy link

@devin-ai-integration devin-ai-integration bot commented Jan 15, 2026

Add archive training day feature with combined ranking

Summary

This PR implements the archive training day feature for training programs. When a training day is archived:

  • Attendance data is extracted and stored (participation status, location based on IP, delay time, delay reasons)
  • Ranking data is extracted and stored (all student tags, task scores, score history for graphs)
  • The contest and participations are permanently deleted
  • The training day remains with name/description/start_time preserved

New database models ArchivedAttendance and ArchivedStudentRanking store the extracted data. The TrainingDay model now has nullable contest_id (SET NULL on delete) and new name/description/start_time/archived_tasks_data/duration fields.

UI changes include:

  • Training days page now separates active vs archived training days
  • Archive button on each active training day
  • Archive confirmation page with class IP selection
  • New "Attendance" tab in training program sidebar showing attendance data across all archived training days
  • Archived training days table shows start time with sortable column
  • New "Combined Ranking" tab showing rankings across all archived training days

Updates since last revision

  • Fix unstable task sorting in ranking views: Tasks are now sorted by training_day_num for stable ordering. The training_day_num is saved to archived_tasks_data during archiving and used to sort tasks in both TrainingProgramCombinedRankingHandler and TrainingProgramCombinedRankingDetailHandler. Falls back to task ID for consistent ordering when training_day_num is not available.

Previous updates (still relevant)

  • Fix submission fetching during archiving: Submissions are now fetched using managing_participation.id (not training day participation), filtered by training_day_id and official=True. This ensures only official submissions made via the specific training day are archived.
  • Fix submission time offset calculation: Time offsets are now calculated relative to the student's starting_time (if available) rather than the contest start time.
  • Store training day duration at archiving: Added duration field to TrainingDay model, calculated as max duration among main groups (if any) or the training day duration (if no main groups).
  • Handle archived training days throughout codebase: Fixed all places where training_day.contest is accessed to handle the case where contest is None (archived):
    • Templates show text instead of links for archived training days
    • Handlers skip operations for archived training days
    • Submission download uses stored description/name
    • ScoringService skips cache re-evaluation for archived training participations
  • Fix graph scale and submission time display in history view: The contest begin/end times were incorrectly set to absolute timestamps, but history and submission times are stored as offsets from contest start. Fixed by setting contest begin to 0 and calculating end from the max history time.
  • Add StudentTask records during archiving: When archiving, StudentTask records are now created for visible tasks that the student doesn't already have.
  • Archive visible tasks based on student tags: Changed archiving to determine which tasks should be visible to each student based on their tags (using can_access_task).
  • Simplified data model: Removed redundant visible_task_ids field from ArchivedStudentRanking. The presence of a task_id key in task_scores now indicates the task was visible.
  • Moved tasks_data to TrainingDay: Task metadata (archived_tasks_data) is now stored once on TrainingDay instead of duplicated per-student.
  • Added submissions field: ArchivedStudentRanking now stores submission data for each task in RWS format.
  • Switched to get_cached_score_entry() for archiving: Per feedback, changed from using task_score() to get_cached_score_entry().
  • Student tags storage: Changed to storing ALL student tags as PostgreSQL ARRAY(Unicode) with a GIN index.
  • Location defaults to "home": When no class IPs are selected during archiving, all participants are now marked as "home".

Review & Testing Checklist for Human

  • Verify task ordering is stable - Archive a training day with multiple tasks, then view the Combined Ranking tab multiple times. Verify tasks always appear in the same order (by training_day_num).

  • Test submission fetching during archiving - Archive a training day and verify that only official submissions made via that training day are included in the archived data (not submissions from other training days or the task archive).

  • Verify submission time offsets - After archiving, check the history view and verify submission times are relative to the student's start time (not contest start). Students who started late should have their submission times adjusted accordingly.

  • Test archived training day handling - Navigate to pages that display training day info (tasks list, submission download, training day remove page) and verify archived training days show their stored name/description instead of crashing.

  • Test history view graphs and submission times - Click "[history]" for a student with submissions and verify: (1) the score/rank graphs display with correct scale, (2) submission times in the table display correctly.

Recommended test plan:

  1. Create a training day with multiple students and tasks (some tasks with visible_to_tags restrictions)
  2. Have some students press start at different times and submit, others NOT press start at all
  3. Archive the training day
  4. Navigate to Combined Ranking tab
  5. Verify: tasks appear in consistent order across page refreshes
  6. Verify: students who missed start still have task columns with 0 scores (based on their tags)
  7. Click "[history]" for a student and verify graphs show correct scale and submission times are relative to their start time
  8. Navigate to training program tasks page and verify archived training day shows name instead of link
  9. Try to remove an archived training day and verify the confirmation page displays correctly
  10. Download submissions and verify archived training day submissions use stored description in folder names

Notes

Summary by CodeRabbit

  • New Features

    • Archive training days with confirmation; preserved attendance and ranking data and admin archive UI.
    • Attendance and combined ranking views for training programs, including per-student detail and date filtering.
  • Enhancements

    • UI separates active vs archived training days; new table sorting and responsive table styles.
    • Training days show name, description, start time, and duration metadata.
    • Safer labels and fallbacks where contests are deleted (archived-day display and download folder names).

✏️ Tip: You can customize this high-level summary in your review settings.

This commit implements the archive training day feature which allows:
- Archiving a training day by extracting attendance and ranking data
- Storing attendance data (status, location, delay time, delay reasons)
- Storing ranking data (tag, task scores, score history)
- Deleting the contest and participations after archiving
- Displaying archived training days separately from active ones
- New Attendance tab to view attendance data for all archived training days

New database models:
- ArchivedAttendance: stores attendance data for archived training days
- ArchivedStudentRanking: stores ranking data for archived training days

Changes to TrainingDay model:
- Added name and description fields (synced with contest while it exists)
- Made contest_id nullable (NULL after archiving)

New handlers:
- ArchiveTrainingDayHandler: handles archiving with IP selection for location
- TrainingProgramAttendanceHandler: displays attendance data with filtering

UI updates:
- Training days page now shows active and archived training days separately
- Archive button added to active training days
- New Attendance tab in training program sidebar
- Archive confirmation page with class IP selection

Co-Authored-By: Ron Ryvchin <[email protected]>
@devin-ai-integration
Copy link
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds archival support: two new ORM models (ArchivedAttendance, ArchivedStudentRanking), TrainingDay schema/relationships updates, admin handlers/templates for archiving and viewing attendance/rankings, CSS for views, and a DB migration creating archival tables and training_days columns plus FK change.

Changes

Cohort / File(s) Summary
Archived models
cms/db/archived_attendance.py, cms/db/archived_student_ranking.py
New ORM models/tables for immutable archived attendance and student ranking with composite unique constraints, FK cascades, indexes, ARRAY/JSONB fields, and relationships to TrainingDay and Student.
Model exports & TrainingDay updates
cms/db/__init__.py, cms/db/training_day.py
Exported new archived models; made training_days.contest_id nullable with ON DELETE SET NULL; added name, description, start_time, duration, archived_tasks_data; added relationships archived_attendances and archived_student_rankings.
Archival handlers & logic
cms/server/admin/handlers/trainingprogram.py
New ArchiveTrainingDayHandler (GET/POST archival flow), helpers to produce ArchivedAttendance/ArchivedStudentRanking, duration calculation, date-filter mixin, and new attendance/combined-ranking handlers and detail/history handlers; minor skip-if-no-contest fix when adding students.
Handler exports & routing
cms/server/admin/handlers/__init__.py
Exposes new handlers and registers routes for archive, attendance, combined_ranking, combined_ranking/history, and combined_ranking_detail endpoints.
Admin templates (archive & views)
cms/server/admin/templates/archive_training_day.html, cms/server/admin/templates/training_program_attendance.html, cms/server/admin/templates/training_program_combined_ranking.html, cms/server/admin/templates/training_program_combined_ranking_detail.html, cms/server/admin/templates/training_program_training_days.html, .../training_program_tasks.html, .../student_tasks.html, .../training_program_task_remove.html, .../training_day_remove.html
New archive confirmation template; attendance and combined-ranking list/detail templates; UI changes to list active vs archived days; many templates now handle training days without contests (fallbacks to training_day.name/description).
Base template & navigation
cms/server/admin/templates/base.html
Sidebar now hides training days without contests and adds Attendance / Combined Ranking menu entries.
Static styles
cms/server/admin/static/aws_style.css
Large CSS addition for attendance & ranking views: responsive headers, filter bar, sticky table headers/first column, status badges, and table layout (note: duplicated blocks and BOM present in file).
Submission & download templates/handlers
cms/server/admin/handlers/submissiondownload.py, cms/server/admin/templates/macro/submission.html, cms/server/contest/templates/task_submissions.html
Folder naming and displayed labels now fall back to training_day.description or training_day.name when contest is missing (archived days).
DB migration
cmscontrib/updaters/update_from_1.5.sql
Creates archived_attendances and archived_student_rankings tables with indexes/constraints; adds name, description, start_time, duration, archived_tasks_data to training_days; changes contest_id FK ON DELETE CASCADE → SET NULL.
Misc handlers
cms/server/contest/handlers/main.py, cms/server/contest/handlers/trainingprogram.py
Logging and overview handlers updated to tolerate missing contest (use training_day.name fallback; skip training days without contest in program overview).

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin UI
    participant Handler as ArchiveTrainingDayHandler
    participant DB as Database
    participant Notify as Notification System

    Admin->>Handler: GET archive page (show shared IPs)
    Admin->>Handler: POST archive request (selected IPs, confirm)
    Handler->>DB: Load TrainingDay & Contest
    Handler->>DB: For each participation -> create ArchivedAttendance records
    Handler->>DB: For each student -> create ArchivedStudentRanking records
    Handler->>DB: Update TrainingDay archival metadata (name/description/start_time/archived_tasks_data/duration)
    Handler->>DB: Delete Contest (cascades participations/submissions)
    alt success
        Handler->>Notify: emit success
        Notify->>Admin: show confirmation
    else failure
        Handler->>Notify: emit error
        Notify->>Admin: show error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-Aga
  • Command-Master

Poem

🐰 I dug a burrow for bygone days,
Stashed scores and names in tidy arrays,
Hopped through templates, handlers, and styles,
Archived each memory across many files —
A rabbit cheers for preserved learning ways!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature being added: archiving training days with combined ranking functionality, which aligns with the substantial changes across database models, handlers, and UI templates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration bot and others added 6 commits January 15, 2026 10:02
- Changed ArchivedStudentRanking.student_tags from JSONB to ARRAY(Unicode)
- Added GIN index for efficient filtering by tag
- Updated archiving logic to store all tags as a list

Co-Authored-By: Ron Ryvchin <[email protected]>
The SQLAlchemy model uses Python-side default=list, not a database-level
DEFAULT, so the SQL migration should not have a DEFAULT clause.

Co-Authored-By: Ron Ryvchin <[email protected]>
…n.ip

The starting_ip_addresses field stores the actual IPs from which students
participated, while participation.ip is for allowed IP restrictions.

Co-Authored-By: Ron Ryvchin <[email protected]>
- Fix 'None' has no attribute 'id' error on training_program_tasks.html
  by checking if training_day.contest is not none before accessing .id
- Fix attendance archiving bug: lookup student by user_id instead of
  participation_id since Student.participation_id points to managing
  contest participation, not training day participation
- Add start_time field to TrainingDay model to store contest start time
  after archiving
- Display start_time in archived training days table with sorting

Co-Authored-By: Ron Ryvchin <[email protected]>
- Fix ScoreHistory.time -> ScoreHistory.timestamp in archiving code
  (the model uses 'timestamp' not 'time')
- Fix sidebar to filter out archived training days (where contest is None)
  to prevent 'None' has no attribute errors when accessing td.contest

Co-Authored-By: Ron Ryvchin <[email protected]>
- Add colored badges with icons for student statuses (On Time, Delayed, Missed)
- Add colored badges for location (Class, Home, Both)
- Color columns within each training day (status, location, reasons)
- Change training day titles to show description with start date in brackets
- Fix date filtering to actually filter archived training days by start_time
- Order results by start_time instead of position
- Add Clear button to reset date filters
- Fix location to default to 'home' when no class IP is chosen during archiving
- Fix location to default to 'home' when participant has no IP recorded

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv
Copy link
Collaborator

ronryv commented Jan 15, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cms/server/admin/templates/training_program_attendance.html`:
- Around line 233-239: The template builds the student edit link with the wrong
ID: it uses student.participation.user.id but the route expects the student ID;
update the link generation to pass student.id (i.e. change the url call in the
table header from student.participation.user.id to student.id so the path
url('training_program', training_program.id, 'student', student.id, 'edit')
points to the correct handler).
🧹 Nitpick comments (6)
cms/server/admin/templates/archive_training_day.html (1)

58-66: Consider adding explicit confirmation for this destructive action.

Since archiving permanently deletes the contest and participations, consider adding an additional confirmation mechanism such as requiring the admin to type the contest name or check a confirmation checkbox. This provides an extra safeguard against accidental archiving.

💡 Optional: Add confirmation checkbox
   <h2>Confirm Archive</h2>
   <p>
+    <label>
+      <input type="checkbox" name="confirm_archive" required />
+      I understand this action is irreversible and will permanently delete the contest
+    </label>
+  </p>
+  <p>
     <input type="submit" value="Archive Training Day"
 {% if not admin.permission_all %}
            disabled
 {% endif %}
     />
   </p>
cms/server/admin/templates/training_program_attendance.html (1)

259-269: Consider adding a fallback for missing location.

When att.location is None, no location is displayed. While the archiving handler should always set a value, adding a fallback would improve robustness.

Optional: Add fallback for None location
                 <div class="location-row">
                   {% if att.location == "class" %}
                     <span class="location-icon">&#x1F3EB;</span> Class
                   {% elif att.location == "home" %}
                     <span class="location-icon">&#x1F3E0;</span> Home
                   {% elif att.location == "both" %}
                     <span class="location-icon">&#x21C4;</span> Both
                   {% elif att.location %}
                     <span>{{ att.location }}</span>
+                  {% else %}
+                    <span class="location-icon">&#x2753;</span> Unknown
                   {% endif %}
                 </div>
cms/db/training_day.py (1)

123-126: Type annotation inconsistent with nullable contest_id.

The contest relationship type hint is "Contest" but since contest_id is now nullable, the relationship can be None after archiving. Consider updating the type annotation for consistency.

Suggested fix
-    contest: "Contest" = relationship(
+    contest: "Contest | None" = relationship(
         "Contest",
         back_populates="training_day",
     )
cms/server/admin/handlers/trainingprogram.py (3)

2175-2175: Consider moving import to top of file.

The DelayRequest import is inside the method. While this works, it's more idiomatic to import at the module level for consistency with other imports.

Suggested change at top of file (around line 30-33)
 from cms.db import Contest, TrainingProgram, Participation, Submission, \
     User, Task, Question, Announcement, Student, StudentTask, Team, \
     TrainingDay, TrainingDayGroup, ArchivedAttendance, ArchivedStudentRanking, \
-    ScoreHistory
+    ScoreHistory, DelayRequest

Then remove the import from line 2175.


2228-2228: Consider moving import to top of file.

Similar to DelayRequest, ParticipationTaskScore is imported inside the method. Consider adding it to the module-level imports for consistency.

Suggested change at top of file (around line 30-33)
 from cms.db import Contest, TrainingProgram, Participation, Submission, \
     User, Task, Question, Announcement, Student, StudentTask, Team, \
     TrainingDay, TrainingDayGroup, ArchivedAttendance, ArchivedStudentRanking, \
-    ScoreHistory
+    ScoreHistory, DelayRequest, ParticipationTaskScore

Then remove the import from line 2228.


2305-2306: Use existing datetime import for timedelta.

The timedelta is imported inside the method, but datetime is already imported as dt at the top. You can use dt module's timedelta or import timedelta alongside at module level.

Suggested fix

Change line 24 to include timedelta:

-from datetime import datetime as dt
+from datetime import datetime as dt, timedelta

Then change lines 2305-2306 to:

-            from datetime import timedelta
             query = query.filter(TrainingDay.start_time < end_date + timedelta(days=1))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6c69f6 and a09184a.

📒 Files selected for processing (12)
  • cms/db/__init__.py
  • cms/db/archived_attendance.py
  • cms/db/archived_student_ranking.py
  • cms/db/training_day.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/templates/archive_training_day.html
  • cms/server/admin/templates/base.html
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/training_program_tasks.html
  • cms/server/admin/templates/training_program_training_days.html
  • cmscontrib/updaters/update_from_1.5.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-09T19:33:59.218Z
Learnt from: ronryv
Repo: ioi-isr/cms PR: 68
File: cms/server/admin/handlers/dataset.py:1323-1387
Timestamp: 2026-01-09T19:33:59.218Z
Learning: In cms/server/admin/handlers/**/*.py, follow the existing pattern: do not emit explicit error notifications when try_commit() fails. Rely on centralized error handling and logging instead. Apply this consistently to all new and updated handlers to maintain uniform behavior and maintainability.

Applied to files:

  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/handlers/__init__.py
🧬 Code graph analysis (4)
cms/db/archived_attendance.py (3)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_day.py (1)
  • TrainingDay (65-156)
cms/db/student.py (1)
  • Student (38-89)
cms/db/__init__.py (2)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-97)
cms/db/training_day.py (2)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-97)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (2)
  • ArchiveTrainingDayHandler (2015-2267)
  • TrainingProgramAttendanceHandler (2270-2341)
🪛 Ruff (0.14.11)
cms/server/admin/handlers/trainingprogram.py

2098-2098: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (25)
cms/server/admin/templates/base.html (2)

313-317: LGTM! Correctly filters out archived training days from sidebar.

The filter if td.contest appropriately excludes archived training days (where contest_id is null) from the sidebar navigation, since they no longer have active contests to link to.


482-482: LGTM! New Attendance navigation entry.

The new menu entry follows the existing pattern and provides navigation to the attendance feature for archived training days.

cms/db/__init__.py (2)

58-58: LGTM! New archived models added to exports.

The new ArchivedAttendance and ArchivedStudentRanking models are correctly added to __all__ alongside related training program models.


114-115: LGTM! Import statements follow existing pattern.

The imports for the new archived data models are consistent with the module's import organization.

cms/server/admin/templates/training_program_tasks.html (1)

76-80: LGTM! Properly handles archived training days without contests.

The updated conditional logic correctly handles the case where a training day exists but its contest has been archived (deleted). The fallback to td.name or "(archived)" provides clear indication to users.

cms/server/admin/templates/training_program_training_days.html (4)

12-13: LGTM! Clear separation of active and archived training days.

The template correctly uses selectattr("contest") to filter active training days (those with associated contests) and presents them in a dedicated section with the ability to archive.

Also applies to: 48-49


63-65: LGTM! Archive action link added.

The archive link correctly routes to the archive confirmation page for each active training day.


82-112: LGTM! Well-structured archived training days section.

The archived section correctly uses rejectattr("contest") to show only archived days, and provides appropriate fallbacks for missing name/description/start_time fields.


114-118: LGTM! Table sorting enabled for archived days.

The sort initialization for the archived table uses correct column index (2 for Start column in the 3-column table).

cms/server/admin/templates/archive_training_day.html (2)

12-21: LGTM! Clear warning about destructive action.

The warning notification clearly communicates the consequences of archiving, including the permanent deletion of the contest and participations. This is important for preventing accidental data loss.


33-56: LGTM! Well-designed class IP selection interface.

The IP selection table is sorted by student count in descending order (most common IPs first), making it easier for admins to identify classroom IPs. The fallback message when no shared IPs exist is appropriate.

cms/db/archived_attendance.py (1)

38-98: LGTM! Well-structured archival model.

The model correctly implements the archival pattern with:

  • Appropriate FK constraints with CASCADE delete behavior
  • Composite unique constraint preventing duplicate records
  • Proper indexing on foreign key columns
  • Clear separation between required (status) and optional (location, delay_time, delay_reasons) fields
cms/server/admin/handlers/__init__.py (1)

187-189: LGTM! Routes and imports follow established patterns.

The new handlers are properly imported and routed, consistent with the existing training program URL structure.

Also applies to: 361-362

cmscontrib/updaters/update_from_1.5.sql (1)

568-664: LGTM! Migration is well-structured and complete.

The SQL migration correctly:

  • Adds archival columns to training_days (name, description, start_time)
  • Changes contest_id from CASCADE to SET NULL for proper archival behavior
  • Creates both archival tables with appropriate constraints, indexes (including GIN for tags), and foreign keys matching the ORM models
cms/db/archived_student_ranking.py (1)

38-97: LGTM! Well-designed archival ranking model.

The model correctly implements:

  • JSONB columns for flexible storage of task scores and score history
  • PostgreSQL ARRAY with GIN index for efficient tag-based filtering
  • Consistent structure with ArchivedAttendance model
  • Proper type hints reflecting the expected data structures
cms/db/training_day.py (3)

86-92: LGTM on the nullable contest_id change.

The change to make contest_id nullable with ondelete="SET NULL" correctly supports the archival workflow where the contest is deleted but the training day record persists.


99-116: LGTM on the new archival fields.

The name, description, and start_time fields appropriately preserve contest metadata after archiving. The comments clearly document the sync-while-active / preserve-after-archive behavior.


146-156: LGTM on the archived relationships.

The cascade="all, delete-orphan" configuration ensures archived attendance and ranking data are properly cleaned up when a training day is deleted.

cms/server/admin/handlers/trainingprogram.py (7)

32-33: LGTM on the new imports.

The imports for ArchivedAttendance, ArchivedStudentRanking, and ScoreHistory are necessary for the archiving functionality.


449-451: LGTM on skipping archived training days.

Correctly guards against attempting to create participations for archived training days that no longer have an associated contest.


2018-2056: LGTM on the archive confirmation page handler.

The validation checks and IP counting logic are well-implemented. The shared IP detection (filtering to IPs with >1 student) provides useful context for the admin to identify class IPs.


2098-2103: Broad exception handling is consistent with existing patterns.

While static analysis flags except Exception, this follows the established handler pattern in this codebase for gracefully handling various failure modes during complex operations. The notification and redirect behavior is appropriate. Based on learnings, the centralized error handling after try_commit() is relied upon for commit failures.


2114-2196: LGTM on attendance archival logic.

The attendance extraction correctly:

  • Looks up students via Participation.user_id join (since Student references managing contest participation)
  • Handles all location edge cases (no class IPs, no participant IPs, mixed IPs)
  • Defaults to "home" when IP data is unavailable
  • Aggregates delay reasons from DelayRequest records

2198-2267: LGTM on ranking archival logic.

The ranking extraction correctly:

  • Preserves all student tags as an array for filtering
  • Stores task scores keyed by task ID (as string for JSON compatibility)
  • Converts score history timestamps to offsets from contest start in RWS format

2270-2341: LGTM on attendance display handler.

The handler correctly:

  • Filters archived training days by date range
  • Builds a student-to-training-day attendance matrix
  • Orders training days by start_time for consistent display
  • Handles missing participation gracefully in the sort key

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@ronryv ronryv force-pushed the devin/1768469828-archive-training-day branch from a09184a to 7b6ee72 Compare January 15, 2026 18:13
- Add TrainingProgramCombinedRankingHandler for main page with date filtering
- Add TrainingProgramCombinedRankingHistoryHandler for JSON history endpoint
- Add TrainingProgramCombinedRankingDetailHandler for student detail view
- Create templates for combined ranking and detail pages
- Add navigation link in sidebar
- Support multi-contest format in history view (each training day is a contest)

Co-Authored-By: Ron Ryvchin <[email protected]>
@devin-ai-integration devin-ai-integration bot changed the title Add archive training day feature Add archive training day feature with combined ranking Jan 15, 2026
ronryv and others added 9 commits January 15, 2026 22:08
The archiving code was incorrectly using contest.tasks (which is empty for
training day contests) instead of training_day.tasks. Also changed to use
the task_score function with the managing contest participation to compute
actual scores, matching how the ranking handler works.

Changes:
- Use training_day.tasks instead of contest.tasks
- Get managing contest participation for each user
- Use task_score() to compute scores filtered by training day
- Filter score history to only include tasks in the training day

Co-Authored-By: Ron Ryvchin <[email protected]>
Changed the archiving code to use get_cached_score_entry which:
1. Uses the score cache efficiently instead of recalculating
2. Handles training day participations correctly (queries submissions
   from managing contest filtered by training_day_id)
3. Uses training_day.tasks instead of contest.tasks

Also fixed ScoreHistory query to use training day participation ID
since history is stored with the training day participation.

Co-Authored-By: Ron Ryvchin <[email protected]>
…nd task metadata

- Store task_scores for ALL visible tasks (including 0 scores) to differentiate
  between inaccessible tasks and 0-score tasks
- Add submissions field to ArchivedStudentRanking for rendering submission tables
- Add archived_tasks_data to TrainingDay to preserve scoring scheme at archive time
- Update combined ranking table to show N/A for inaccessible tasks
- Update combined ranking detail page to use archived task metadata and submissions
- Simplify data model: use task_scores keys for visibility instead of separate field

Co-Authored-By: Ron Ryvchin <[email protected]>
…tTask records

When archiving a training day, determine which tasks should be visible to each
student based on their tags (using can_access_task), rather than only including
tasks where the student already has a StudentTask record. This ensures students
who missed the start button still get the training day's tasks in their archive
with 0 scores.

Co-Authored-By: Ron Ryvchin <[email protected]>
When archiving a training day, create StudentTask records for visible tasks
that the student doesn't already have. This allows students who missed the
training to still submit those tasks from home via the training program
management contest, mirroring the behavior of the start button.

Co-Authored-By: Ron Ryvchin <[email protected]>
The contest begin/end times were set to absolute timestamps (td.start_time),
but the history and submission times are stored as offsets from the original
contest start. This caused:
1. Graph scale to be incorrect (data points outside the visible range)
2. Submission times to display incorrectly (negative values)

Fix: Set contest begin to 0 and calculate end from the max history time,
since all times are stored as offsets from contest start.

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv
Copy link
Collaborator

ronryv commented Jan 16, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cms/server/admin/static/aws_style.css`:
- Around line 1223-1292: The global selectors .page-header and .btn-primary in
aws_style.css are overriding Bootstrap site-wide; scope them by nesting or
renaming—e.g., change selectors to .training-program-view .page-header and
.training-program-view .btn-primary (or rename to .tp-page-header and
.tp-btn-primary) and update any templates that render training program pages to
wrap content in the .training-program-view container so only
training-program-specific headers and primary buttons get the new flex and teal
styles while leaving Bootstrap's defaults elsewhere; keep other classes like
.filter-card/.filter-group/.btn-text unchanged.

In `@cms/server/admin/templates/training_program_combined_ranking.html`:
- Around line 86-91: The empty-state colspan currently uses "{{ 1 +
archived_training_days|length }}" which only covers the date column and archived
training day columns; update it to compute the full number of table columns by
adding the task columns as well (e.g. replace with "{{ 1 +
archived_training_days|length + tasks|length }}" or the correct task-list
variable used in this template) so the "No students found" row spans all task
columns and keeps the table aligned.
🧹 Nitpick comments (2)
cmscontrib/updaters/update_from_1.5.sql (1)

568-572: Consider populating name and description for existing rows.

After adding these nullable columns, existing training_days rows will have NULL values for name and description. If the application code expects these to be populated (even for active training days with contests), you may want to add an UPDATE statement to initialize them from the associated contest data.

Proposed migration addition
-- After adding the columns, populate from existing contest data
UPDATE public.training_days td
SET name = c.name,
    description = c.description
FROM public.contests c
WHERE td.contest_id = c.id
  AND td.name IS NULL;
cms/server/admin/handlers/trainingprogram.py (1)

2385-2413: Consider extracting date filtering logic.

The date parsing and archived training day query pattern is duplicated across TrainingProgramAttendanceHandler, TrainingProgramCombinedRankingHandler, TrainingProgramCombinedRankingHistoryHandler, and TrainingProgramCombinedRankingDetailHandler.

A helper method could reduce duplication:

♻️ Example helper extraction
def _parse_date_range(self) -> tuple[dt | None, dt | None]:
    """Parse start_date and end_date query arguments."""
    start_date = end_date = None
    start_str = self.get_argument("start_date", None)
    end_str = self.get_argument("end_date", None)
    if start_str:
        try:
            start_date = dt.fromisoformat(start_str)
        except ValueError:
            pass
    if end_str:
        try:
            end_date = dt.fromisoformat(end_str)
        except ValueError:
            pass
    return start_date, end_date

def _get_archived_training_days(
    self, training_program_id: int,
    start_date: dt | None, end_date: dt | None
) -> list[TrainingDay]:
    """Query archived training days with optional date filtering."""
    query = (
        self.sql_session.query(TrainingDay)
        .filter(TrainingDay.training_program_id == training_program_id)
        .filter(TrainingDay.contest_id.is_(None))
    )
    if start_date:
        query = query.filter(TrainingDay.start_time >= start_date)
    if end_date:
        query = query.filter(TrainingDay.start_time < end_date + timedelta(days=1))
    return query.order_by(TrainingDay.start_time).all()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a09184a and 88f6397.

📒 Files selected for processing (14)
  • cms/db/archived_student_ranking.py
  • cms/db/training_day.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/static/aws_style.css
  • cms/server/admin/templates/archive_training_day.html
  • cms/server/admin/templates/base.html
  • cms/server/admin/templates/student_tasks.html
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/training_program_combined_ranking.html
  • cms/server/admin/templates/training_program_combined_ranking_detail.html
  • cms/server/admin/templates/training_program_task_remove.html
  • cms/server/contest/templates/task_submissions.html
  • cmscontrib/updaters/update_from_1.5.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/archive_training_day.html
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-09T19:33:59.218Z
Learnt from: ronryv
Repo: ioi-isr/cms PR: 68
File: cms/server/admin/handlers/dataset.py:1323-1387
Timestamp: 2026-01-09T19:33:59.218Z
Learning: In cms/server/admin/handlers/**/*.py, follow the existing pattern: do not emit explicit error notifications when try_commit() fails. Rely on centralized error handling and logging instead. Apply this consistently to all new and updated handlers to maintain uniform behavior and maintainability.

Applied to files:

  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/handlers/__init__.py
🧬 Code graph analysis (4)
cms/db/training_day.py (5)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/training_day_group.py (1)
  • TrainingDayGroup (37-82)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-106)
cms/server/admin/handlers/trainingprogram.py (8)
cms/db/contest.py (1)
  • Contest (47-384)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/training_day.py (1)
  • TrainingDay (66-165)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-106)
cms/db/scorecache.py (1)
  • ScoreHistory (120-168)
cms/server/util.py (3)
  • get_all_student_tags (64-79)
  • deduplicate_preserving_order (436-451)
  • can_access_task (157-193)
cms/server/contest/handlers/trainingprogram.py (1)
  • get (44-167)
cms/db/archived_student_ranking.py (3)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_day.py (1)
  • TrainingDay (66-165)
cms/db/student.py (1)
  • Student (38-89)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (6)
  • RemoveTrainingDayGroupHandler (1731-1749)
  • ArchiveTrainingDayHandler (2030-2374)
  • TrainingProgramAttendanceHandler (2377-2447)
  • TrainingProgramCombinedRankingHandler (2450-2543)
  • TrainingProgramCombinedRankingHistoryHandler (2546-2595)
  • TrainingProgramCombinedRankingDetailHandler (2598-2776)
🪛 Ruff (0.14.11)
cms/server/admin/handlers/trainingprogram.py

2113-2113: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (debian:bookworm)
  • GitHub Check: test (ubuntu:noble)
🔇 Additional comments (26)
cms/server/admin/static/aws_style.css (2)

1294-1382: Sticky header + first-column implementation looks solid.

Good use of sticky positioning, z-index layering, and overflow handling to keep headers and student names visible.


1384-1498: Attendance & combined ranking visual styles look good.

The badge system, date/task headers, and score cell differentiation are clear and consistent with the rest of the admin theme.

cms/server/admin/templates/training_program_task_remove.html (1)

11-14: LGTM!

The additional task.training_day.contest check correctly guards against accessing .contest.description when the training day has been archived (contest_id is NULL). This aligns well with the nullable contest_id schema change.

cms/server/admin/templates/base.html (2)

313-317: LGTM!

The filter if td.contest correctly excludes archived training days (where contest_id is NULL) from the sidebar navigation. This prevents displaying broken links and aligns with the schema change making contest_id nullable.


482-483: LGTM!

New navigation entries for Attendance and Combined Ranking tabs are correctly placed within the training program menu section.

cmscontrib/updaters/update_from_1.5.sql (3)

582-620: LGTM!

The archived_attendances table schema is well-designed:

  • Unique constraint on (training_day_id, student_id) prevents duplicate entries
  • Proper foreign keys with CASCADE delete for referential integrity
  • Appropriate indexes on both foreign key columns

621-661: LGTM!

The archived_student_rankings table is properly structured:

  • GIN index on student_tags array enables efficient tag-based queries
  • JSONB columns for flexible storage of scores, submissions, and history
  • Consistent constraint and index patterns with the attendance table

663-668: LGTM!

Adding start_time and archived_tasks_data columns completes the archive metadata storage requirements for training days.

cms/server/contest/templates/task_submissions.html (1)

411-411: LGTM!

The fallback chain training_day.contest.description if training_day.contest else (training_day.description or training_day.name) correctly handles archived training days where the contest has been deleted. This ensures a meaningful description is always displayed.

cms/server/admin/templates/student_tasks.html (1)

57-61: LGTM!

The conditional logic correctly handles archived training days:

  • When contest exists: renders a clickable link to the contest
  • When contest is NULL: displays the training day's description/name with a sensible "Archived Training Day" fallback

This prevents broken links and provides clear context for archived sources.

cms/server/admin/templates/training_program_combined_ranking.html (1)

8-22: Filter controls look good.
Clear date inputs + reset link make the flow straightforward for admins.

cms/server/admin/templates/training_program_combined_ranking_detail.html (1)

317-412: JS wiring for the archived detail view looks solid.
The override/init flow cleanly adapts the existing renderer to archived data.

cms/db/training_day.py (1)

87-125: Archival metadata fields align with the new workflow.
Persisting name/description/start_time/tasks data after contest deletion is consistent with the archive semantics.

cms/db/archived_student_ranking.py (1)

38-105: Model definition looks consistent with the archival requirements.
Constraints and column structure are coherent for per‑student historical data.

cms/server/admin/handlers/__init__.py (1)

364-368: Routes for archive/attendance/combined ranking views are wired cleanly.

cms/server/admin/handlers/trainingprogram.py (11)

24-49: LGTM!

Import additions are well-organized and align with the new archiving functionality. The utilities (can_access_task, get_all_student_tags, etc.) and models (ArchivedAttendance, ArchivedStudentRanking, etc.) are appropriately imported for the new handlers.


464-466: LGTM!

Good defensive check to skip archived training days (where contest is None) when adding a new student. This prevents attempting to create participations for contests that no longer exist.


2030-2071: LGTM!

The GET handler correctly validates ownership, checks archive status, and computes shared IPs for class IP selection. The logic to identify IPs with more than one student (count > 1) effectively filters for likely classroom IPs.


2098-2127: Archive operation is well-structured.

The archiving flow correctly:

  1. Preserves contest metadata on the TrainingDay before deletion
  2. Archives attendance and ranking data
  3. Deletes the contest (cascading to participations)

The broad Exception catch at line 2113 (flagged by static analysis) follows the existing codebase pattern for form submission handlers. Based on learnings, this is consistent with how other handlers in this file manage errors.


2129-2210: LGTM!

The attendance archival logic correctly handles:

  • Student lookup via user_id (since Student.participation_id references the managing contest, not training day)
  • Location determination with proper handling of edge cases (no class IPs, no IP recorded, mixed IPs)
  • Delay reason aggregation from DelayRequest records

2212-2298: Well-designed ranking archival logic.

Key aspects are correctly implemented:

  • can_access_task determines per-student task visibility based on tags
  • StudentTask records are created for visible tasks, enabling students who missed training to submit later
  • Task metadata is preserved in archived_tasks_data for historical accuracy

The inline import of get_cached_score_entry at line 2228 is acceptable for avoiding potential circular imports.


2299-2374: LGTM!

The score and submission data collection is well-implemented:

  • Time offsets are calculated consistently from contest.start
  • String keys for task_scores align with JSONB storage patterns
  • Nullable handling (if task_scores else None) avoids storing empty dicts

2377-2447: LGTM!

Attendance display handler correctly:

  • Filters for archived training days (contest_id.is_(None))
  • Applies optional date range filtering
  • Builds a nested dict structure for easy template access
  • Gracefully handles students without participation records in sort key

2546-2595: LGTM!

History endpoint correctly aggregates score history entries across archived training days and returns them in the expected JSON format for graph rendering.


2598-2776: Well-implemented detail view handler.

The handler correctly:

  • Calculates graph scale with begin=0 and end from max history time (fixing the submission time display per PR description)
  • Preserves original scoring scheme via archived_tasks_data
  • Builds comprehensive data structures for the detail template

Note: The Query.get() usage at line 2683 has the same SQLAlchemy 2.0 deprecation concern flagged above.


2512-2520: : The codebase uses SQLAlchemy 1.3.24 (constrained to <1.4), where Query.get() is the standard and correct pattern. Deprecation warnings for Query.get() only apply to SQLAlchemy 2.0+. This pattern is used consistently throughout the codebase, including in utility functions in db/base.py, and requires no changes.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

devin-ai-integration bot and others added 2 commits January 17, 2026 10:44
1. Fix submission fetching to use managing_participation.id, training_day_id,
   and official=True filter when archiving
2. Fix submission time offset to be relative to student start time
3. Add duration field to TrainingDay model and calculate at archiving time
   (max of main groups duration or training day duration)
4. Handle archived training days (where contest is None) in all places:
   - ScoringService: already checks contest_id is not None
   - Templates: show text instead of links for archived training days
   - Handlers: skip operations for archived training days
   - Submission download: use stored description/name for archived training days

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv
Copy link
Collaborator

ronryv commented Jan 17, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@cms/server/admin/handlers/trainingprogram.py`:
- Around line 2678-2738: The task ordering is unstable because
task_ids_in_contest is iterated as a set; change the loop that currently reads
"for task_id in task_ids_in_contest:" to iterate over a sorted list (e.g., "for
task_id in sorted(task_ids_in_contest):") so tasks/columns are produced
deterministically; update any related aggregation (contest_tasks,
contest_max_score, submissions_data population) to rely on this sorted order so
the detailed ranking view remains stable (refer to task_ids_in_contest,
contest_tasks, contest_max_score, and the loop body that builds
tasks_data/submissions_data).
- Around line 2532-2585: The UI columns can shuffle because task_scores (JSONB)
keys are unordered; in TrainingProgramCombinedRankingHandler when building
training_day_tasks from visible_tasks_by_id you should produce a deterministic
ordering — e.g., sort the tasks before assigning training_day_tasks[td.id].
Locate the block that creates visible_tasks_by_id and replace the final
assignment training_day_tasks[td.id] = list(visible_tasks_by_id.values()) with a
sorted list (stable key such as task id or name) so training_day_tasks[td.id]
always has tasks in the same order regardless of JSON key order; ensure any
other consumption of visible_tasks_by_id follows the same sorted order.
- Around line 2080-2131: Replace the broad "except Exception as error" in the
post method with a catch for SQLAlchemyError (or a more specific sqlalchemy.exc
subclass) so programming errors still propagate; import SQLAlchemyError from
sqlalchemy.exc, change the handler to "except SQLAlchemyError as error:", keep
the existing service.add_notification and redirect logic there, and do not
swallow other exceptions (remove the generic Exception handler) so non-database
errors surface to centralized error handling; reference the post method, the
sql_session.delete(contest) call, and service.add_notification when making this
change.

In `@cms/server/admin/static/aws_style.css`:
- Line 1: Remove the leading BOM character before the html selector in
aws_style.css so the selector reads "html {" without any invisible characters;
locate the start of the file (the "html" selector) and delete the BOM (UTF-8 BOM
/ U+FEFF) so the linter no longer sees an invalid selector.

In `@cms/server/contest/templates/task_submissions.html`:
- Line 411: The header's td_desc expression can end up empty when both
training_day.description and training_day.name are blank; update the td_desc
fallback chain in the trans block so it includes a final literal fallback (e.g.,
"Archived training day" or "Unnamed training day") after checking
training_day.contest.description, training_day.description, and
training_day.name so the H3 never renders blank—modify the td_desc computation
used in the <h3> (training_day, td_desc) to append that final fallback string.
♻️ Duplicate comments (3)
cms/server/admin/templates/training_program_attendance.html (1)

47-48: Use the student ID in the edit link.
The edit route expects the student ID, but the template passes the user ID. This can lead to 404s or editing the wrong record.

🐛 Proposed fix
-            {{ student.participation.user.first_name }} {{ student.participation.user.last_name }} (<a href="{{ url('training_program', training_program.id, 'student', student.participation.user.id, 'edit') }}">{{ student.participation.user.username }}</a>)
+            {{ student.participation.user.first_name }} {{ student.participation.user.last_name }} (<a href="{{ url('training_program', training_program.id, 'student', student.id, 'edit') }}">{{ student.participation.user.username }}</a>)
cms/server/admin/templates/training_program_combined_ranking_detail.html (1)

200-204: Use the student ID in the edit link.
The route expects the student ID, but the template passes the user ID.

🐛 Proposed fix
-  <a href="{{ url("training_program", training_program.id, "student", student.participation.user.id, "edit") }}">Edit Student</a>
+  <a href="{{ url("training_program", training_program.id, "student", student.id, "edit") }}">Edit Student</a>
cms/server/admin/templates/training_program_combined_ranking.html (1)

82-85: Use the student ID in the edit link.
The edit route expects the student ID, but the template passes the user ID.

🐛 Proposed fix
-                <a class="student-name-link" href="{{ url('training_program', training_program.id, 'student', student.participation.user.id, 'edit') }}">
+                <a class="student-name-link" href="{{ url('training_program', training_program.id, 'student', student.id, 'edit') }}">

ronryv and others added 3 commits January 17, 2026 14:11
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@ronryv ronryv merged commit 1c5fb37 into training_program Jan 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants